Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Node / ReticulumProjects / MeshChatX.git / files / meshchatx / src / backend / interface_editor.py

Displaying Raw • Download


meshchatx/src/backend/interface_editor.py c0b0ddffd735a0d007c1be9a5e8563ec42fdafc0 (c0b0ddff) Text, 14.22 KB

T8b949e# SPDX-License-Identifier: 0BSD AND MIT

Tff7b72import T7ee787os
Tff7b72import T7ee787re

Tff7b72import T7ee787RNS

Te6edf3_IPV4_HOST_PORT Tff7b72= Te6edf3reTff7b72.Td2a8ffcompileTb4b4b4(Ta5d6ffrTa5d6ff"Ta5d6ff^(Ta5d6ff\Ta5d6ffdTa5d6ff{Ta5d6ff1,3}(?:Ta5d6ff\Ta5d6ff.Ta5d6ff\Ta5d6ffdTa5d6ff{Ta5d6ff1,3})Tffd700{3}Ta5d6ff):(Ta5d6ff\Ta5d6ffdTa5d6ff{Ta5d6ff1,5})$Ta5d6ff"Tb4b4b4)

T8b949e# Canonical Reticulum interface mode strings (RNS 1.3.7+ includes internal).
Te6edf3ALLOWED_INTERFACE_MODES Tff7b72= Tffa657frozensetTb4b4b4(
Tb4b4b4{
Ta5d6ff"Ta5d6fffullTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffgatewayTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffgwTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffaccess_pointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffaccesspointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffapTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpointtopointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffptpTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffroamingTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffboundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffinternalTa5d6ff"Tb4b4b4,
Tb4b4b4}
Tb4b4b4)

T8b949e# Prefer writing the long form when aliases are supplied via the API.
Te6edf3_INTERFACE_MODE_CANONICAL Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6ffgwTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffgatewayTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffaccesspointTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffaccess_pointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffapTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffaccess_pointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpointtopointTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffpointtopointTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffptpTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffpointtopointTa5d6ff"Tb4b4b4,
Tb4b4b4}

Te6edf3_YES_NO_TRUE Tff7b72= Tffa657frozensetTb4b4b4(Tb4b4b4{Ta5d6ff"Ta5d6fftrueTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffyesTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff1Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffonTa5d6ff"Tb4b4b4}Tb4b4b4)
Te6edf3_YES_NO_FALSE Tff7b72= Tffa657frozensetTb4b4b4(Tb4b4b4{Ta5d6ff"Ta5d6fffalseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffnoTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff0Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffoffTa5d6ff"Tb4b4b4}Tb4b4b4)

T8b949e# location_cmd is executed by RNS Discovery via subprocess.run([path]).
T8b949e# Reject shell metacharacters and relative traversal before persisting.
Te6edf3_LOCATION_CMD_FORBIDDEN Tff7b72= Te6edf3reTff7b72.Td2a8ffcompileTb4b4b4(Ta5d6ffrTa5d6ff"Ta5d6ff[Ta5d6ff\Ta5d6ffx00-Ta5d6ff\Ta5d6ffx1fTa5d6ff\Ta5d6ffx7f;&|`$<>Tffea00\\Tffea00\"Ta5d6ff'Ta5d6ff*?Ta5d6ff\Ta5d6ff[Ta5d6ff\Ta5d6ff]Tffd700{}Ta5d6ff()!#]Ta5d6ff"Tb4b4b4)


Tff7b72def Td2a8ffnormalize_rnode_tcp_portTb4b4b4(Te6edf3portTb4b4b4: Tffa657strTb4b4b4) Tff7b72-Tff7b72> Tffa657strTb4b4b4:
T8b949e"""Normalize RNodeInterface port when using tcp://.

Reticulum's TCPConnection (RNS/Interfaces/RNodeInterface.py) calls
socket.getaddrinfo(target_host, 7633). The first argument must be a hostname or IP **only**; an embedded :port
breaks resolution. Config may list legacy tcp://host:7633 or tcp://host:;
strip those so storage matches tcp://<host>.
"""
Te6edf3raw Tff7b72= Tffa657strTb4b4b4(Te6edf3portTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3low Tff7b72= Te6edf3rawTff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Te6edf3scheme Tff7b72= Ta5d6ff"Ta5d6fftcp://Ta5d6ff"
Tff7b72if Tff7b72not Te6edf3lowTff7b72.Td2a8ffstartswithTb4b4b4(Te6edf3schemeTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3raw
Te6edf3rest Tff7b72= Te6edf3rawTb4b4b4[Tffa657lenTb4b4b4(Te6edf3schemeTb4b4b4) Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72while Te6edf3restTff7b72.Td2a8ffendswithTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3rest Tff7b72= Te6edf3restTb4b4b4[Tb4b4b4:Tff7b72-T79c0ff1Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3restTb4b4b4:
Tff7b72return Te6edf3scheme
Tff7b72if Te6edf3restTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ff[Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3close Tff7b72= Te6edf3restTff7b72.Td2a8fffindTb4b4b4(Ta5d6ff"Ta5d6ff]Ta5d6ff"Tb4b4b4)
Tff7b72if Te6edf3close Tff7b72!= Tff7b72-T79c0ff1 Tff7b72and Tffa657lenTb4b4b4(Te6edf3restTb4b4b4) Tff7b72> Te6edf3close Tff7b72+ T79c0ff1 Tff7b72and Te6edf3restTb4b4b4[Te6edf3close Tff7b72+ T79c0ff1Tb4b4b4] Tff7b72== Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4:
Te6edf3tail Tff7b72= Te6edf3restTb4b4b4[Te6edf3close Tff7b72+ T79c0ff2 Tb4b4b4:Tb4b4b4]
Tff7b72if Te6edf3tailTff7b72.Td2a8ffisdigitTb4b4b4(Tb4b4b4) Tff7b72and T79c0ff1 Tff7b72<Tff7b72= Tffa657intTb4b4b4(Te6edf3tailTb4b4b4) Tff7b72<Tff7b72= T79c0ff65535Tb4b4b4:
Te6edf3rest Tff7b72= Te6edf3restTb4b4b4[Tb4b4b4: Te6edf3close Tff7b72+ T79c0ff1Tb4b4b4]
Tff7b72return Te6edf3scheme Tff7b72+ Te6edf3rest
Te6edf3m Tff7b72= Te6edf3_IPV4_HOST_PORTTff7b72.Td2a8ffmatchTb4b4b4(Te6edf3restTb4b4b4)
Tff7b72if Te6edf3m Tff7b72and Tffa657intTb4b4b4(Te6edf3mTff7b72.Td2a8ffgroupTb4b4b4(T79c0ff2Tb4b4b4)Tb4b4b4) Tff7b72<Tff7b72= T79c0ff65535Tb4b4b4:
Te6edf3rest Tff7b72= Te6edf3mTff7b72.Td2a8ffgroupTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elif Te6edf3restTff7b72.Td2a8ffcountTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4) Tff7b72== T79c0ff1Tb4b4b4:
Te6edf3headTb4b4b4, Te6edf3tail Tff7b72= Te6edf3restTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4, T79c0ff1Tb4b4b4)
Tff7b72if Te6edf3tailTff7b72.Td2a8ffisdigitTb4b4b4(Tb4b4b4) Tff7b72and T79c0ff1 Tff7b72<Tff7b72= Tffa657intTb4b4b4(Te6edf3tailTb4b4b4) Tff7b72<Tff7b72= T79c0ff65535Tb4b4b4:
Te6edf3rest Tff7b72= Te6edf3head
Tff7b72return Te6edf3scheme Tff7b72+ Te6edf3rest


Tff7b72def Td2a8ffcoerce_rnode_frequency_hzTb4b4b4(Te6edf3valueTb4b4b4)Tb4b4b4:
T8b949e"""Return RNode carrier frequency as integer Hz for Reticulum config.

Reticulum reads frequency with int(); MHz-style decimals (868.825)
must not be stored verbatim or they truncate to invalid values. Accepts
Hz integers, bare MHz-style numbers below 1e6, and strings with optional
ghz/mhz/khz/hz suffix (ASCII, case-insensitive).
"""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Te6edf3value
Te6edf3raw Tff7b72= Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3s Tff7b72= Te6edf3rawTff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)Tff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Ta5d6ff_Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)
Te6edf3mult Tff7b72= T79c0ff1.0
Tff7b72for Te6edf3suffixTb4b4b4, Te6edf3m Tff7b72in Tb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffghzTa5d6ff"Tb4b4b4, T79c0ff1e9Tb4b4b4)Tb4b4b4, Tb4b4b4(Ta5d6ff"Ta5d6ffmhzTa5d6ff"Tb4b4b4, T79c0ff1e6Tb4b4b4)Tb4b4b4, Tb4b4b4(Ta5d6ff"Ta5d6ffkhzTa5d6ff"Tb4b4b4, T79c0ff1e3Tb4b4b4)Tb4b4b4, Tb4b4b4(Ta5d6ff"Ta5d6ffhzTa5d6ff"Tb4b4b4, T79c0ff1.0Tb4b4b4)Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3sTff7b72.Td2a8ffendswithTb4b4b4(Te6edf3suffixTb4b4b4)Tb4b4b4:
Te6edf3s Tff7b72= Te6edf3sTb4b4b4[Tb4b4b4: Tff7b72-Tffa657lenTb4b4b4(Te6edf3suffixTb4b4b4)Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3mult Tff7b72= Te6edf3m
Tff7b72break
Te6edf3f Tff7b72= Tffa657floatTb4b4b4(Te6edf3sTb4b4b4) Tff7b72* Te6edf3mult
Tff7b72if Te6edf3f Tff7b72<Tff7b72= T79c0ff0Tb4b4b4:
Tff7b72return Tffa657intTb4b4b4(Tffa657roundTb4b4b4(Te6edf3fTb4b4b4)Tb4b4b4)
Tff7b72if Te6edf3f Tff7b72>Tff7b72= T79c0ff1_000_000Tb4b4b4:
Tff7b72return Tffa657intTb4b4b4(Tffa657roundTb4b4b4(Te6edf3fTb4b4b4)Tb4b4b4)
Te6edf3is_integer Tff7b72= Tffa657absTb4b4b4(Te6edf3f Tff7b72- Tffa657roundTb4b4b4(Te6edf3fTb4b4b4)Tb4b4b4) Tff7b72< T79c0ff1e-9
Tff7b72if Tb4b4b4(Tff7b72not Te6edf3is_integerTb4b4b4) Tff7b72or Tb4b4b4(Te6edf3is_integer Tff7b72and Te6edf3f Tff7b72< T79c0ff10_000Tb4b4b4)Tb4b4b4:
Te6edf3f Tff7b72*Tff7b72= T79c0ff1_000_000.0
Tff7b72return Tffa657intTb4b4b4(Tffa657roundTb4b4b4(Te6edf3fTb4b4b4)Tb4b4b4)


Te6edf3RNODE_TXPOWER_MIN Tff7b72= T79c0ff0
Te6edf3RNODE_TXPOWER_MAX Tff7b72= T79c0ff37


Tff7b72def Td2a8ffnormalize_rnode_txpowerTb4b4b4(Te6edf3valueTb4b4b4)Tb4b4b4:
T8b949e"""Return integer dBm for Reticulum RNodeInterface config."""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Te6edf3value
Tff7b72return Tffa657intTb4b4b4(Tffa657floatTb4b4b4(Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)


Tff7b72def Td2a8ffvalidate_rnode_txpowerTb4b4b4(Te6edf3valueTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Return an API error message when TX power is invalid for Reticulum."""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffTX power is requiredTa5d6ff"
Tff7b72tryTb4b4b4:
Te6edf3power Tff7b72= Te6edf3normalize_rnode_txpowerTb4b4b4(Te6edf3valueTb4b4b4)
Tff7b72except Tb4b4b4(Tf85149TypeErrorTb4b4b4, Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffTX power must be an integer dBm valueTa5d6ff"
Tff7b72if Tff7b72not Tffa657isinstanceTb4b4b4(Te6edf3powerTb4b4b4, Tffa657intTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffTX power must be an integer dBm valueTa5d6ff"
Tff7b72if Te6edf3power Tff7b72< Te6edf3RNODE_TXPOWER_MIN Tff7b72or Te6edf3power Tff7b72> Te6edf3RNODE_TXPOWER_MAXTb4b4b4:
Tff7b72return Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffTX power must be between Tffd700{Te6edf3RNODE_TXPOWER_MINTffd700}Ta5d6ff and Tffd700{Te6edf3RNODE_TXPOWER_MAXTffd700}Ta5d6ff dBm Ta5d6ff"
Ta5d6ff"Ta5d6ff(Reticulum RNodeInterface limit; typical SX1262 range is 0-22 dBm)Ta5d6ff"
Tb4b4b4)
Tff7b72return Tff7b72None


Tff7b72class T56d364InterfaceEditorTb4b4b4:
Te6edf3coerce_rnode_frequency_hz Tff7b72= Tffa657staticmethodTb4b4b4(Te6edf3coerce_rnode_frequency_hzTb4b4b4)
Te6edf3normalize_rnode_tcp_port Tff7b72= Tffa657staticmethodTb4b4b4(Te6edf3normalize_rnode_tcp_portTb4b4b4)
Te6edf3normalize_rnode_txpower Tff7b72= Tffa657staticmethodTb4b4b4(Te6edf3normalize_rnode_txpowerTb4b4b4)
Te6edf3validate_rnode_txpower Tff7b72= Tffa657staticmethodTb4b4b4(Te6edf3validate_rnode_txpowerTb4b4b4)

Tf0883e@staticmethod
Tff7b72def Td2a8ffsanitize_interface_section_nameTb4b4b4(Te6edf3nameTb4b4b4: Tffa657str Tff7b72| Tff7b72NoneTb4b4b4) Tff7b72-Tff7b72> Tffa657strTb4b4b4:
T8b949e"""Make a name safe for Reticulum/ConfigObj [[section]] headers.

Square brackets break ConfigObj nesting and can leave the in-memory
interfaces map dirty after a failed write, blocking later adds.
"""
Te6edf3cleaned Tff7b72= Tffa657strTb4b4b4(Te6edf3name Tff7b72or Ta5d6ff"Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3cleanedTb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ff"
Te6edf3cleaned Tff7b72= Tb4b4b4(
Te6edf3cleanedTff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Ta5d6ff[Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff(Ta5d6ff"Tb4b4b4)
Tff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Ta5d6ff]Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff)Ta5d6ff"Tb4b4b4)
Tff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff Ta5d6ff"Tb4b4b4)
Tff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Tffea00\rTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff Ta5d6ff"Tb4b4b4)
Tb4b4b4)
Te6edf3cleaned Tff7b72= Ta5d6ff"Ta5d6ff Ta5d6ff"Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3cleanedTff7b72.Td2a8ffsplitTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72return Te6edf3cleanedTb4b4b4[Tb4b4b4:T79c0ff128Tb4b4b4]

Tf0883e@staticmethod
Tff7b72def Td2a8ffminimum_fixed_mtuTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657intTb4b4b4:
Te6edf3mtu Tff7b72= Tffa657getattrTb4b4b4(Te6edf3RNSTff7b72.Td2a8ffReticulumTb4b4b4, Ta5d6ff"Ta5d6ffMTUTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Tffa657typeTb4b4b4(Te6edf3mtuTb4b4b4) Tff7b72is Tffa657int Tff7b72and Te6edf3mtu Tff7b72> T79c0ff0Tb4b4b4:
Tff7b72return Te6edf3mtu
Tff7b72return T79c0ff500

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_fixed_mtuTb4b4b4(Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4, Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist fixed_mtu when valid; return an API error message otherwise."""
Te6edf3value Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6fffixed_mtuTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Ta5d6ff"Ta5d6fffixed_mtuTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72return Tff7b72None
Tff7b72tryTb4b4b4:
Te6edf3mtu Tff7b72= Tffa657intTb4b4b4(Te6edf3valueTb4b4b4)
Tff7b72except Tb4b4b4(Tf85149TypeErrorTb4b4b4, Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6fffixed_mtu must be a positive integerTa5d6ff"
Te6edf3min_mtu Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffminimum_fixed_mtuTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3mtu Tff7b72< Te6edf3min_mtuTb4b4b4:
Tff7b72return Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6fffixed_mtu must be at least Tffd700{Te6edf3min_mtuTffd700}Ta5d6ff bytes Ta5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ff(Reticulum minimum MTU; values below this prevent startup)Ta5d6ff"
Tb4b4b4)
Te6edf3interface_detailsTb4b4b4[Ta5d6ff"Ta5d6fffixed_mtuTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3mtu
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffupdate_valueTb4b4b4(Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4, Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4, Te6edf3keyTb4b4b4: Tffa657strTb4b4b4)Tb4b4b4:
T8b949e# update value if provided and not empty
Te6edf3value Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Te6edf3keyTb4b4b4)
Tff7b72if Te6edf3value Tff7b72is Tff7b72not Tff7b72None Tff7b72and Te6edf3value Tff7b72!= Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3interface_detailsTb4b4b4[Te6edf3keyTb4b4b4] Tff7b72= Te6edf3value
Tff7b72return

T8b949e# otherwise remove existing value
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Te6edf3keyTb4b4b4, Tff7b72NoneTb4b4b4)

Tf0883e@staticmethod
Tff7b72def Td2a8ffnormalize_interface_modeTb4b4b4(Te6edf3valueTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Return a canonical Reticulum mode string, or None when unset."""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Tff7b72None
Te6edf3mode Tff7b72= Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3mode Tff7b72not Tff7b72in Te6edf3ALLOWED_INTERFACE_MODESTb4b4b4:
Tff7b72return Tff7b72None
Tff7b72return Te6edf3_INTERFACE_MODE_CANONICALTff7b72.Td2a8ffgetTb4b4b4(Te6edf3modeTb4b4b4, Te6edf3modeTb4b4b4)

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_interface_modeTb4b4b4(Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4, Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist mode when valid. Return an API error message otherwise."""
Tff7b72if Ta5d6ff"Ta5d6ffmodeTa5d6ff" Tff7b72not Tff7b72in Te6edf3dataTb4b4b4:
Tff7b72return Tff7b72None
Te6edf3value Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffmodeTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Ta5d6ff"Ta5d6ffmodeTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72return Tff7b72None
Te6edf3mode Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffnormalize_interface_modeTb4b4b4(Te6edf3valueTb4b4b4)
Tff7b72if Te6edf3mode Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tb4b4b4(
Ta5d6ff"Ta5d6ffmode must be one of: full, gateway, access_point, Ta5d6ff"
Ta5d6ff"Ta5d6ffpointtopoint, roaming, boundary, internalTa5d6ff"
Tb4b4b4)
Te6edf3interface_detailsTb4b4b4[Ta5d6ff"Ta5d6ffmodeTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3mode
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffrequest_yes_noTb4b4b4(Te6edf3valueTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Map common truthy/falsey request values to Reticulum yes/no."""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Tff7b72None
Tff7b72if Tffa657isinstanceTb4b4b4(Te6edf3valueTb4b4b4, Tffa657boolTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffyesTa5d6ff" Tff7b72if Te6edf3value Tff7b72else Ta5d6ff"Ta5d6ffnoTa5d6ff"
Te6edf3s Tff7b72= Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3s Tff7b72in Te6edf3_YES_NO_TRUETb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffyesTa5d6ff"
Tff7b72if Te6edf3s Tff7b72in Te6edf3_YES_NO_FALSETb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6ffnoTa5d6ff"
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_yes_no_optionTb4b4b4(
Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4,
Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4,
Te6edf3keyTb4b4b4: Tffa657strTb4b4b4,
Tff7b72*Tb4b4b4,
Te6edf3default_when_missingTb4b4b4: Tffa657str Tff7b72| Tff7b72None Tff7b72= Tff7b72NoneTb4b4b4,
Tb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist a Reticulum yes/no option. Return error text on bad input.

When the key is absent from data, leave existing config alone unless
default_when_missing is set (then write that yes/no or pop when None).
"""
Tff7b72if Te6edf3key Tff7b72not Tff7b72in Te6edf3dataTb4b4b4:
Tff7b72if Te6edf3default_when_missing Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tff7b72None
Tff7b72if Te6edf3default_when_missing Tff7b72in Tb4b4b4(Ta5d6ff"Ta5d6ffyesTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffnoTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3interface_detailsTb4b4b4[Te6edf3keyTb4b4b4] Tff7b72= Te6edf3default_when_missing
Tff7b72elseTb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Te6edf3keyTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72return Tff7b72None
Te6edf3yn Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffrequest_yes_noTb4b4b4(Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Te6edf3keyTb4b4b4)Tb4b4b4)
Tff7b72if Te6edf3yn Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Te6edf3keyTb4b4b4, Tff7b72NoneTb4b4b4)
Te6edf3raw Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Te6edf3keyTb4b4b4)
Tff7b72if Te6edf3raw Tff7b72is Tff7b72None Tff7b72or Te6edf3raw Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Tff7b72None
Tff7b72return Ta5d6fffTa5d6ff"Tffd700{Te6edf3keyTffd700}Ta5d6ff must be a boolean or yes/no valueTa5d6ff"
Te6edf3interface_detailsTb4b4b4[Te6edf3keyTb4b4b4] Tff7b72= Te6edf3yn
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffvalidate_location_cmdTb4b4b4(Te6edf3valueTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Return an error when location_cmd is unsafe for RNS Discovery exec."""
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Tff7b72return Tff7b72None
Te6edf3raw Tff7b72= Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3rawTb4b4b4:
Tff7b72return Tff7b72None
Tff7b72if Te6edf3_LOCATION_CMD_FORBIDDENTff7b72.Td2a8ffsearchTb4b4b4(Te6edf3rawTb4b4b4)Tb4b4b4:
Tff7b72return Tb4b4b4(
Ta5d6ff"Ta5d6fflocation_cmd must be an absolute executable path without Ta5d6ff"
Ta5d6ff"Ta5d6ffshell metacharacters or control charactersTa5d6ff"
Tb4b4b4)
Tff7b72if Ta5d6ff"Ta5d6ff..Ta5d6ff" Tff7b72in Te6edf3rawTff7b72.Td2a8ffreplaceTb4b4b4(Ta5d6ff"Tffea00\\Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff/Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6fflocation_cmd must not contain parent-directory segmentsTa5d6ff"
Te6edf3expanded Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(Te6edf3rawTb4b4b4)
Tff7b72if Tff7b72not Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisabsTb4b4b4(Te6edf3expandedTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6ff"Ta5d6fflocation_cmd must be an absolute path or start with ~/Ta5d6ff"
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_location_cmdTb4b4b4(Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4, Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist discovery location_cmd when valid."""
Tff7b72if Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff" Tff7b72not Tff7b72in Te6edf3dataTb4b4b4:
Tff7b72return Tff7b72None
Te6edf3value Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72return Tff7b72None
Te6edf3error Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffvalidate_location_cmdTb4b4b4(Te6edf3valueTb4b4b4)
Tff7b72if Te6edf3error Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72return Te6edf3error
Te6edf3interface_detailsTb4b4b4[Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffnormpathTb4b4b4(
Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(Tffa657strTb4b4b4(Te6edf3valueTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_positive_numberTb4b4b4(
Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4,
Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4,
Te6edf3keyTb4b4b4: Tffa657strTb4b4b4,
Tff7b72*Tb4b4b4,
Te6edf3as_intTb4b4b4: Tffa657bool Tff7b72= Tff7b72FalseTb4b4b4,
Te6edf3minimumTb4b4b4: Tffa657float Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3maximumTb4b4b4: Tffa657float Tff7b72| Tff7b72None Tff7b72= Tff7b72NoneTb4b4b4,
Tb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist a numeric option with bounds. Return error text if invalid."""
Tff7b72if Te6edf3key Tff7b72not Tff7b72in Te6edf3dataTb4b4b4:
Tff7b72return Tff7b72None
Te6edf3value Tff7b72= Te6edf3dataTff7b72.Td2a8ffgetTb4b4b4(Te6edf3keyTb4b4b4)
Tff7b72if Te6edf3value Tff7b72is Tff7b72None Tff7b72or Te6edf3value Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3interface_detailsTff7b72.Td2a8ffpopTb4b4b4(Te6edf3keyTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72return Tff7b72None
Tff7b72tryTb4b4b4:
Te6edf3number Tff7b72= Tffa657intTb4b4b4(Te6edf3valueTb4b4b4) Tff7b72if Te6edf3as_int Tff7b72else Tffa657floatTb4b4b4(Te6edf3valueTb4b4b4)
Tff7b72except Tb4b4b4(Tf85149TypeErrorTb4b4b4, Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Tff7b72return Ta5d6fffTa5d6ff"Tffd700{Te6edf3keyTffd700}Ta5d6ff must be a numberTa5d6ff"
Tff7b72if Te6edf3number Tff7b72< Te6edf3minimumTb4b4b4:
Tff7b72return Ta5d6fffTa5d6ff"Tffd700{Te6edf3keyTffd700}Ta5d6ff must be at least Tffd700{Te6edf3minimumTffd700}Ta5d6ff"
Tff7b72if Te6edf3maximum Tff7b72is Tff7b72not Tff7b72None Tff7b72and Te6edf3number Tff7b72> Te6edf3maximumTb4b4b4:
Tff7b72return Ta5d6fffTa5d6ff"Tffd700{Te6edf3keyTffd700}Ta5d6ff must be at most Tffd700{Te6edf3maximumTffd700}Ta5d6ff"
Te6edf3interface_detailsTb4b4b4[Te6edf3keyTb4b4b4] Tff7b72= Tffa657intTb4b4b4(Te6edf3numberTb4b4b4) Tff7b72if Te6edf3as_int Tff7b72else Te6edf3number
Tff7b72return Tff7b72None

Tf0883e@staticmethod
Tff7b72def Td2a8ffapply_backbone_fast_flappingTb4b4b4(
Te6edf3interface_detailsTb4b4b4: Tffa657dictTb4b4b4,
Te6edf3dataTb4b4b4: Tffa657dictTb4b4b4,
Tb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Persist BackboneInterface fast-flapping options (RNS 1.4.0)."""
Te6edf3err Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffapply_yes_no_optionTb4b4b4(
Te6edf3interface_detailsTb4b4b4,
Te6edf3dataTb4b4b4,
Ta5d6ff"Ta5d6ffblock_fast_flappingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72if Te6edf3errTb4b4b4:
Tff7b72return Te6edf3err
Te6edf3err Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffapply_positive_numberTb4b4b4(
Te6edf3interface_detailsTb4b4b4,
Te6edf3dataTb4b4b4,
Ta5d6ff"Ta5d6fffast_flapping_block_timeTa5d6ff"Tb4b4b4,
Te6edf3as_intTff7b72=Tff7b72TrueTb4b4b4,
Te6edf3minimumTff7b72=T79c0ff1Tb4b4b4,
Te6edf3maximumTff7b72=T79c0ff60 Tff7b72* T79c0ff24 Tff7b72* T79c0ff30Tb4b4b4,
Tb4b4b4)
Tff7b72if Te6edf3errTb4b4b4:
Tff7b72return Te6edf3err
Te6edf3err Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffapply_positive_numberTb4b4b4(
Te6edf3interface_detailsTb4b4b4,
Te6edf3dataTb4b4b4,
Ta5d6ff"Ta5d6fffast_flapping_thresholdTa5d6ff"Tb4b4b4,
Te6edf3as_intTff7b72=Tff7b72FalseTb4b4b4,
Te6edf3minimumTff7b72=T79c0ff0.1Tb4b4b4,
Te6edf3maximumTff7b72=T79c0ff3600Tb4b4b4,
Tb4b4b4)
Tff7b72if Te6edf3errTb4b4b4:
Tff7b72return Te6edf3err
Tff7b72return Te6edf3InterfaceEditorTff7b72.Td2a8ffapply_positive_numberTb4b4b4(
Te6edf3interface_detailsTb4b4b4,
Te6edf3dataTb4b4b4,
Ta5d6ff"Ta5d6fffast_flapping_graceTa5d6ff"Tb4b4b4,
Te6edf3as_intTff7b72=Tff7b72TrueTb4b4b4,
Te6edf3minimumTff7b72=T79c0ff0Tb4b4b4,
Te6edf3maximumTff7b72=T79c0ff10_000Tb4b4b4,
Tb4b4b4)

Tf0883e@staticmethod
Tff7b72def Td2a8ffsanitize_imported_rns_optionsTb4b4b4(Te6edf3iface_bodyTb4b4b4: Tffa657dictTb4b4b4) Tff7b72-Tff7b72> Tffa657str Tff7b72| Tff7b72NoneTb4b4b4:
T8b949e"""Normalize/validate RNS 1.3.7+ options on import. Return error or None."""
Tff7b72if Ta5d6ff"Ta5d6ffmodeTa5d6ff" Tff7b72in Te6edf3iface_bodyTb4b4b4:
Te6edf3mode Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffnormalize_interface_modeTb4b4b4(Te6edf3iface_bodyTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffmodeTa5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72if Te6edf3mode Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tb4b4b4(
Ta5d6ff"Ta5d6ffImported interface mode must be one of: full, gateway, Ta5d6ff"
Ta5d6ff"Ta5d6ffaccess_point, pointtopoint, roaming, boundary, internalTa5d6ff"
Tb4b4b4)
Te6edf3iface_bodyTb4b4b4[Ta5d6ff"Ta5d6ffmodeTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3mode
Tff7b72for Te6edf3key Tff7b72in Tb4b4b4(Ta5d6ff"Ta5d6ffrecursive_prsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffannounces_from_internalTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffblock_fast_flappingTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3key Tff7b72not Tff7b72in Te6edf3iface_bodyTb4b4b4:
Tff7b72continue
Te6edf3yn Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffrequest_yes_noTb4b4b4(Te6edf3iface_bodyTff7b72.Td2a8ffgetTb4b4b4(Te6edf3keyTb4b4b4)Tb4b4b4)
Tff7b72if Te6edf3yn Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Ta5d6fffTa5d6ff"Ta5d6ffImported interface Tffd700{Te6edf3keyTffd700}Ta5d6ff must be a boolean or yes/no valueTa5d6ff"
Te6edf3iface_bodyTb4b4b4[Te6edf3keyTb4b4b4] Tff7b72= Te6edf3yn
Tff7b72if Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff" Tff7b72in Te6edf3iface_bodyTb4b4b4:
Te6edf3loc Tff7b72= Te6edf3iface_bodyTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3loc Tff7b72is Tff7b72None Tff7b72or Te6edf3loc Tff7b72== Ta5d6ff"Ta5d6ff"Tb4b4b4:
Te6edf3iface_bodyTff7b72.Td2a8ffpopTb4b4b4(Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3error Tff7b72= Te6edf3InterfaceEditorTff7b72.Td2a8ffvalidate_location_cmdTb4b4b4(Te6edf3locTb4b4b4)
Tff7b72if Te6edf3error Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72return Te6edf3error
Te6edf3iface_bodyTb4b4b4[Ta5d6ff"Ta5d6fflocation_cmdTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffnormpathTb4b4b4(
Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(Tffa657strTb4b4b4(Te6edf3locTb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72return Tff7b72None


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────